package cn.okcode.slowjson.parser;

import cn.okcode.slowjson.internal.JsonException;

import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.Objects;

/**
 * 字符读取类
 * <p>
 * <p>
 * 基于jsoup的CharacterReader类修改  --jsoup是个html解析器
 */
public final class CharacterReader implements AutoCloseable {
    public static final char EOF = (char) -1;
    private static final int maxStringCacheLen = 12;
    static final int maxBufferLen = 1024 * 32; // visible for testing
    static final int readAheadLimit = (int) (maxBufferLen * 0.75); // visible for testing
    private static final int minReadAheadLen = 1024; // the minimum mark length supported. No HTML entities can be larger than this.

    private char[] charBuf;
    private Reader reader;
    private int bufLength;
    private int bufSplitPoint;
    private int bufPos;
    private int readerPos;
    private int bufMark = -1;
    private static final int stringCacheSize = 512;
    private String[] stringCache = new String[stringCacheSize]; // holds reused strings in this doc, to lessen garbage

    public CharacterReader(Reader input, int sz) {
        Objects.requireNonNull(input);
        if (!input.markSupported()) {
            throw new IllegalArgumentException("Reader对象必须支持位置标记");
        }
        reader = input;
        charBuf = new char[Math.min(sz, maxBufferLen)];
        bufferUp();
    }

    public CharacterReader(Reader input) {
        this(input, maxBufferLen);
    }

    public CharacterReader(String input) {
        this(new StringReader(input), input.length());
    }

    @Override
    public void close() {
        if (reader == null)
            return;
        try {
            reader.close();
        } catch (IOException ignored) {
        } finally {
            reader = null;
            charBuf = null;
            stringCache = null;
        }
    }

    private boolean readFully; // if the underlying stream has been completely read, no value in further buffering

    private void bufferUp() {
        if (readFully || bufPos < bufSplitPoint)
            return;

        final int pos;
        final int offset;
        if (bufMark != -1) {
            pos = bufMark;
            offset = bufPos - bufMark;
        } else {
            pos = bufPos;
            offset = 0;
        }

        try {
            final long skipped = reader.skip(pos);
            reader.mark(maxBufferLen);
            int read = 0;
            while (read <= minReadAheadLen) {
                int thisRead = reader.read(charBuf, read, charBuf.length - read);
                if (thisRead == -1)
                    readFully = true;
                if (thisRead <= 0)
                    break;
                read += thisRead;
            }
            reader.reset();
            if (read > 0) {
                // Previously asserted that there is room in buf to skip, so this will be a WTF
                if (skipped != pos) {
                    throw new IllegalArgumentException("跳过的字符串与期望的不一致");
                }
                bufLength = read;
                readerPos += pos;
                bufPos = offset;
                if (bufMark != -1)
                    bufMark = 0;
                bufSplitPoint = Math.min(bufLength, readAheadLimit);
            }
        } catch (IOException e) {
            throw new JsonException(e);
        }
    }

    /**
     * Gets the current cursor position in the content.
     *
     * @return current position
     */
    public int pos() {
        return readerPos + bufPos;
    }

    /**
     * Tests if all the content has been read.
     *
     * @return true if nothing left to read.
     */
    public boolean isEmpty() {
        bufferUp();
        return bufPos >= bufLength;
    }

    private boolean isEmptyNoBufferUp() {
        return bufPos >= bufLength;
    }

    /**
     * Get the char at the current position.
     *
     * @return char
     */
    public char current() {
        bufferUp();
        return isEmptyNoBufferUp() ? EOF : charBuf[bufPos];
    }

    public char next() {
        advance();
        return current();
    }

    /**
     * Moves the current position by one.
     */
    public void advance() {
        bufPos++;
    }

    /**
     * Returns the number of characters between the current position and the next instance of the input char
     *
     * @param c scan target
     * @return offset between current position and next instance of target. -1 if not found.
     */
    int nextIndexOf(char c) {
        // doesn't handle scanning for surrogates
        bufferUp();
        for (int i = bufPos; i < bufLength; i++) {
            if (c == charBuf[i])
                return i - bufPos;
        }
        return -1;
    }

    /**
     * Reads characters up to the specific char.
     *
     * @param c the delimiter
     * @return the chars read
     */
    public String consumeTo(char c) {
        int offset = nextIndexOf(c);
        if (offset != -1) {
            String consumed = cacheString(charBuf, stringCache, bufPos, offset);
            bufPos += offset;
            return consumed;
        } else {
            return consumeToEnd();
        }
    }

    /**
     * Read characters until the first of any delimiters is found.
     *
     * @param chars delimiters to scan for
     * @return characters read up to the matched delimiter.
     */
    public String consumeToAny(final char... chars) {
        bufferUp();
        int pos = bufPos;
        final int start = pos;
        final int remaining = bufLength;
        final char[] val = charBuf;
        final int charLen = chars.length;
        int i;

        OUTER:
        while (pos < remaining) {
            for (i = 0; i < charLen; i++) {
                if (val[pos] == chars[i])
                    break OUTER;
            }
            pos++;
        }

        bufPos = pos;
        return pos > start ? cacheString(charBuf, stringCache, start, pos - start) : "";
    }

    String consumeToEnd() {
        bufferUp();
        String data = cacheString(charBuf, stringCache, bufPos, bufLength - bufPos);
        bufPos = bufLength;
        return data;
    }

    //记录是否为浮点数
    boolean isFloat;

    String consumeNumber() {
        isFloat = false;
        bufferUp();
        int start = bufPos;

        //尝试读取符号
        char ch = current();
        if (ch == '-' || ch == '+') {
            ch = next();
        }

        //尝试读取纯数字
        int numCount = 0;
        while (ch >= '0' && ch <= '9') {
            ch = next();
            numCount++;
        }

        //尝试读取小数
        if (ch == '.') {
            isFloat = true;
            while (true) {
                ch = next();
                if (ch >= '0' && ch <= '9') {
                    numCount++;
                } else {
                    break;
                }
            }
        }

        if (numCount == 0) {
            throw new NumberFormatException("数值格式错误，数字长度为零");
        }

        if (ch == 'L') {
            //长整形
            ch = next();
        } else if (ch == 'F') {
            //单精度浮点
            ch = next();
            isFloat = true;
        } else if (ch == 'D') {
            //双精度浮点
            ch = next();
            isFloat = true;
        } else if (ch == 'e' || ch == 'E') {
            //科学计数法
            ch = next();
            if (ch == '+' || ch == '-') {
                ch = next();
            }
            while (ch >= '0' && ch <= '9') {
                ch = next();
            }

            if (ch == 'D' || ch == 'F') {
                ch = next();
            }
            isFloat = true;
        }
        if (bufPos - start == 0) {
            throw new NumberFormatException("empty String");
        }
        return cacheString(charBuf, stringCache, start, bufPos - start);
    }

    public final static char[] ESCAPES = new char[128];

    static {
        ESCAPES['0'] = '\0';
        ESCAPES['1'] = '\1';
        ESCAPES['2'] = '\2';
        ESCAPES['3'] = '\3';
        ESCAPES['4'] = '\4';
        ESCAPES['5'] = '\5';
        ESCAPES['6'] = '\6';
        ESCAPES['7'] = '\7';
        ESCAPES['b'] = '\b';
        ESCAPES['t'] = '\t';
        ESCAPES['n'] = '\n';
        ESCAPES['f'] = '\f';
        ESCAPES['r'] = '\r';
        ESCAPES['"'] = '\"';
        ESCAPES['\\'] = '\\';
        ESCAPES['v'] = '\u000B';
        ESCAPES['/'] = '/';
    }

    /**
     * 以下代码从fastjson弄过来~
     * <p>
     * digits数组说明：
     * 数组下标是字符的ASCII值，数组值是字符对应的16进制值
     * 即：
     * 0-9:
     * digits['0'] = 0;
     * ...
     * digits['9'] = 9;
     * <p>
     * a,b,c,d,e,f
     * digits['a'] = 10;
     * ...
     * digits['f'] = 15;
     * <p>
     * A,B,C,D,E,F:
     * digits['A'] = 10;
     * ...
     * digits['F'] = 15;
     * <p>
     * 只考虑0-9的时候 和下面方法等效：
     * Byte.parseByte(String.valueOf('7'))
     * Integer.parseInt(new String(new char[]{'4'}))
     */
    private final static int[] digits = new int[(int) 'f' + 1];

    static {
        for (int i = '0'; i <= '9'; ++i) {
            digits[i] = i - '0';
        }
        for (int i = 'a'; i <= 'f'; ++i) {
            digits[i] = (i - 'a') + 10;
        }
        for (int i = 'A'; i <= 'F'; ++i) {
            digits[i] = (i - 'A') + 10;
        }
    }

    private final StringBuilder builder = new StringBuilder();

    public String consumeString() {
        bufferUp();
        boolean foundEscape = false;
        int start = bufPos;
        while (bufPos < bufLength) {
            char c = current();
            if (c == '"') {
                break;
            } else if (c == '\\') {
                bufPos++;
                if (bufPos >= bufLength) {
                    throw new JsonException("不能识别的字符：'\\'");
                }
                char ch = current();
                if (ESCAPES.length > ch && ESCAPES[ch] != 0) {
                    builder.append(ESCAPES[ch]);
                } else if (ch == 'x') {
                    advance();
                    char x1 = next();
                    char x2 = next();

                    int x_val = digits[x1] * 16 + digits[x2];
                    char x_char = (char) x_val;
                    builder.append(x_char);
                    break;
                } else if (ch == 'u') {
                    char c1 = next();
                    char c2 = next();
                    char c3 = next();
                    char c4 = next();
                    int val = Integer.parseInt(new String(new char[]{c1, c2, c3, c4}), 16);
                    builder.append((char) val);
                    break;
                } else {
                    throw new JsonException("unclosed string : " + ch);
                }
                bufPos++;
                foundEscape = true;
            } else {
                builder.append(current());
                bufPos++;
            }
        }
        if (foundEscape) {
            String value = builder.toString();
            builder.setLength(0);
            return value;
        } else {
            builder.setLength(0);
            return cacheString(charBuf, stringCache, start, bufPos - start);
        }
    }

    char findFirst() {
        bufferUp();
        for (char ch : charBuf) {
            switch (ch) {
                case '\t':
                case '\r':
                case '\n':
                case '\f':
                    break;
                default:
                    return ch;
            }
        }
        return EOF;
    }

    boolean matches(String seq) {
        bufferUp();
        int scanLength = seq.length();
        if (scanLength > bufLength - bufPos)
            return false;

        for (int offset = 0; offset < scanLength; offset++)
            if (seq.charAt(offset) != charBuf[bufPos + offset])
                return false;
        return true;
    }

    boolean matchConsume(String seq) {
        bufferUp();
        if (matches(seq)) {
            bufPos += seq.length();
            return true;
        } else {
            return false;
        }
    }

    @Override
    public String toString() {
        if (bufLength - bufPos < 0)
            return "";
        return new String(charBuf, bufPos, bufLength - bufPos);
    }

    /**
     * Caches short strings, as a flywheel pattern, to reduce GC load. Just for this doc, to prevent leaks.
     * <p/>
     * Simplistic, and on hash collisions just falls back to creating a new string, vs a full HashMap with Entry list.
     * That saves both having to create objects as hash keys, and running through the entry list, at the expense of
     * some more duplicates.
     */
    private static String cacheString(final char[] charBuf, final String[] stringCache, final int start,
                                      final int count) {
        // limit (no cache):
        if (count > maxStringCacheLen)
            return new String(charBuf, start, count);
        if (count < 1)
            return "";

        // calculate hash:
        int hash = 31 * count;
        int offset = start;
        for (int i = 0; i < count; i++) {
            hash = 31 * hash + charBuf[offset++];
        }

        // get from cache
        final int index = hash & stringCacheSize - 1;
        String cached = stringCache[index];

        if (cached == null) { // miss, add
            cached = new String(charBuf, start, count);
            stringCache[index] = cached;
        } else { // hashcode hit, check equality
            if (rangeEquals(charBuf, start, count, cached)) { // hit
                return cached;
            } else { // hashcode conflict
                cached = new String(charBuf, start, count);
                stringCache[index] = cached; // update the cache, as recently used strings are more likely to show up again
            }
        }
        return cached;
    }

    /**
     * Check if the value of the provided range equals the string.
     */
    static boolean rangeEquals(final char[] charBuf, final int start, int count, final String cached) {
        if (count == cached.length()) {
            int i = start;
            int j = 0;
            while (count-- != 0) {
                if (charBuf[i++] != cached.charAt(j++))
                    return false;
            }
            return true;
        }
        return false;
    }
}
